diff --git a/Device/InputOutput/OutputDataReadWriteNicos.cpp b/Device/InputOutput/OutputDataReadWriteNicos.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..669987496bdce29ec9caaf2e8912edce055c03c0
--- /dev/null
+++ b/Device/InputOutput/OutputDataReadWriteNicos.cpp
@@ -0,0 +1,147 @@
+//  ************************************************************************************************
+//
+//  BornAgain: simulate and fit reflection and scattering
+//
+//! @file      Device/InputOutput/OutputDataReadWriteNicos.cpp
+//! @brief     Implements class OutputDataReadWriteNicos
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see COPYING)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
+//
+//  ************************************************************************************************
+
+#include "Device/InputOutput/OutputDataReadWriteNicos.h"
+#include "Base/Utils/StringUtils.h"
+
+OutputData<double>* OutputDataReadWriteNicos::readOutputData(std::istream& input_stream)
+{
+    OutputData<double>* result = new OutputData<double>;
+    std::string line;
+    m_currentLineNr = 0;
+
+    int m_width = 0;
+    int m_height = 0;
+
+    // -- read dimensions
+    bool inFileSection = false;
+    bool fileSectionFound = false;
+    while (std::getline(input_stream, line)) {
+        m_currentLineNr++;
+        line = StringUtils::trimFront(line, " ");
+        if (!inFileSection && StringUtils::startsWith(line, "%File")) {
+            inFileSection = true;
+            fileSectionFound = true;
+            continue;
+        }
+
+        if (!inFileSection)
+            continue;
+
+        if (StringUtils::startsWith(line, "%")) {
+            inFileSection = false;
+            break;
+        }
+
+        if (StringUtils::startsWith(line, "DataSizeX") && inFileSection) {
+            m_width = readAssignedIntValue(line);
+        } else if (StringUtils::startsWith(line, "DataSizeY") && inFileSection) {
+            m_height = readAssignedIntValue(line);
+        }
+
+        if (m_width != 0 && m_height != 0)
+            break;
+    }
+
+    if (!fileSectionFound)
+        throw std::runtime_error("Could not find 'File' section.");
+    if (m_width == 0)
+        throw std::runtime_error("Could not find DataSizeX value.");
+    if (m_height == 0)
+        throw std::runtime_error("Could not find DataSizeY value.");
+
+    result->addAxis("x", m_width, 0.0, m_width);
+    result->addAxis("y", m_height, 0.0, m_height);
+
+    // -- read data
+    bool inCountSection = false;
+    bool countSectionFound = false;
+    int dataRow = 0;
+    std::vector<unsigned> axes_indices(2);
+
+    while (std::getline(input_stream, line)) {
+        m_currentLineNr++;
+        line = StringUtils::trimFront(line, " ");
+        if (!inCountSection && StringUtils::startsWith(line, "%Counts")) {
+            inCountSection = true;
+            countSectionFound = true;
+            continue;
+        }
+
+        if (!inCountSection)
+            continue;
+
+        if (StringUtils::startsWith(line, "%")) {
+            inCountSection = false;
+            break;
+        }
+
+        // line is a data line
+        line = StringUtils::trim(line, " ");
+        if (line.empty())
+            continue;
+
+        const auto valuesAsString = StringUtils::split(line, ",");
+        if (valuesAsString.size() != m_width)
+            throw std::runtime_error(lineRelatedError(
+                "Number of found values (" + std::to_string(valuesAsString.size())
+                + ") does not match DataSizeX (" + std::to_string(m_width) + ")."));
+
+        for (unsigned col = 0; col < m_width; ++col) {
+            axes_indices[0] = col;
+            axes_indices[1] = static_cast<unsigned>(m_height) - 1
+                              - dataRow; // #baNicos check this (taken from TIFF)
+            const size_t global_index = result->toGlobalIndex(axes_indices);
+
+            int value = 0;
+            if (!StringUtils::to_int(valuesAsString[col], &value))
+                throw std::runtime_error(lineRelatedError(
+                    "Value '" + valuesAsString[col] + "' could not be converted to integer."));
+
+            (*result)[global_index] = value;
+        }
+        dataRow++;
+
+        if (dataRow == m_height)
+            break;
+    }
+
+    if (!countSectionFound)
+        throw std::runtime_error("Could not find 'Counts' section.");
+    if (dataRow != m_height)
+        throw std::runtime_error("Number of found data rows (" + std::to_string(dataRow)
+                                 + ") does not match DataSizeY (" + std::to_string(m_height)
+                                 + ").");
+
+    return result;
+}
+
+int OutputDataReadWriteNicos::readAssignedIntValue(const std::string& line) const
+{
+    auto parts = StringUtils::split(line, "=");
+    if (parts.size() != 2)
+        throw std::runtime_error(lineRelatedError("Missing assigned value."));
+
+    int value = 0;
+    if (!StringUtils::to_int(parts[1], &value))
+        throw std::runtime_error(
+            lineRelatedError("Can't parse assigned value '" + parts[1] + "'."));
+
+    return value;
+}
+
+std::string OutputDataReadWriteNicos::lineRelatedError(const std::string& errorText) const
+{
+    return "Line " + std::to_string(m_currentLineNr) + ": " + errorText;
+}
diff --git a/Device/InputOutput/OutputDataReadWriteNicos.h b/Device/InputOutput/OutputDataReadWriteNicos.h
new file mode 100644
index 0000000000000000000000000000000000000000..dfa1c8dd6e149f7aa912969a93804b91fcc8c993
--- /dev/null
+++ b/Device/InputOutput/OutputDataReadWriteNicos.h
@@ -0,0 +1,38 @@
+//  ************************************************************************************************
+//
+//  BornAgain: simulate and fit reflection and scattering
+//
+//! @file      Device/InputOutput/OutputDataReadWriteNicos.h
+//! @brief     Defines class OutputDataReadWriteNicos
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see COPYING)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
+//
+//  ************************************************************************************************
+
+#ifndef BORNAGAIN_DEVICE_INPUTOUTPUT_OUTPUTDATAREADWRITENICOS_H
+#define BORNAGAIN_DEVICE_INPUTOUTPUT_OUTPUTDATAREADWRITENICOS_H
+
+#include "Device/Data/OutputData.h"
+
+//! Read/write Nicos files (*.001).
+//! @ingroup input_output_internal
+
+class OutputDataReadWriteNicos {
+public:
+    OutputData<double>* readOutputData(std::istream& input_stream);
+
+private:
+    //! Reads the assigned integer value from a line content like "DataSizeX = 100"
+    //! Throws if not successful
+    int readAssignedIntValue(const std::string& line) const;
+
+    //! Returns errorText with prepended line number (suitable for throwing errors)
+    std::string lineRelatedError(const std::string& errorText) const;
+
+    int m_currentLineNr = 0; //!< "1" means "first line" (human readable counting)
+};
+
+#endif // BORNAGAIN_DEVICE_INPUTOUTPUT_OUTPUTDATAREADWRITENICOS_H