diff --git a/openbis_ng_ui/package.json b/openbis_ng_ui/package.json
index 7396f78297977a52564b57a57d43b1e1a318c7df..c98328cf991b7925534c8e41435a9441d768395d 100644
--- a/openbis_ng_ui/package.json
+++ b/openbis_ng_ui/package.json
@@ -10,7 +10,9 @@
     "@material-ui/icons": "^4.9.1",
     "@material-ui/lab": "^4.0.0-alpha.56",
     "@material-ui/pickers": "^3.3.10",
+    "csv": "^5.5.3",
     "date-fns": "^2.22.1",
+    "file-saver": "^2.0.5",
     "history": "^4.10.1",
     "install": "^0.13.0",
     "npm": "^6.14.8",
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridController.js b/openbis_ng_ui/src/js/components/common/grid/GridController.js
index 00d47d0697a26e487edbcc8dfe0784c90c78836c..209e7c240f4bcb03cd8217aa262dc2f5700d26a5 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridController.js
+++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js
@@ -1,6 +1,9 @@
 import _ from 'lodash'
 import autoBind from 'auto-bind'
+import FileSaver from 'file-saver'
+import CsvStringify from 'csv-stringify'
 import compare from '@src/js/common/compare.js'
+import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js'
 
 export default class GridController {
   constructor() {
@@ -293,7 +296,8 @@ export default class GridController {
       filterable: column.filterable === undefined ? true : column.filterable,
       visible: column.visible === undefined ? true : column.visible,
       configurable:
-        column.configurable === undefined ? true : column.configurable
+        column.configurable === undefined ? true : column.configurable,
+      exportable: column.exportable === undefined ? true : column.exportable
     }
   }
 
@@ -658,6 +662,122 @@ export default class GridController {
     }
   }
 
+  async handleExport(options) {
+    function _stringToUtf16ByteArray(str) {
+      var bytes = []
+      bytes.push(255, 254)
+      for (var i = 0; i < str.length; ++i) {
+        var charCode = str.charCodeAt(i)
+        bytes.push(charCode & 0xff) //low byte
+        bytes.push((charCode & 0xff00) >>> 8) //high byte (might be 0)
+      }
+      return bytes
+    }
+
+    function _exportColumnsFromData(namePrefix, data, headings) {
+      var arrayOfRowArrays = []
+      arrayOfRowArrays.push(headings)
+      for (var dIdx = 0; dIdx < data.length; dIdx++) {
+        var rowAsArray = []
+        for (var hIdx = 0; hIdx < headings.length; hIdx++) {
+          var headerKey = headings[hIdx]
+          var rowValue = data[dIdx][headerKey]
+          if (!rowValue) {
+            rowValue = ''
+          } else {
+            var specialCharsRemover = document.createElement('textarea')
+            specialCharsRemover.innerHTML = rowValue
+            rowValue = specialCharsRemover.value //Removes special HTML Chars
+            rowValue = String(rowValue).replace(/\r?\n|\r|\t/g, ' ') //Remove carriage returns and tabs
+
+            if (options.values === GridExportOptions.RICH_TEXT) {
+              // do nothing with the value
+            } else if (options.values === GridExportOptions.PLAIN_TEXT) {
+              rowValue = String(rowValue).replace(/<(?:.|\n)*?>/gm, '')
+            } else {
+              throw Error('Unsupported values option: ' + options.values)
+            }
+          }
+          rowAsArray.push(rowValue)
+        }
+        arrayOfRowArrays.push(rowAsArray)
+      }
+
+      CsvStringify(
+        {
+          header: false,
+          delimiter: '\t',
+          quoted: false
+        },
+        arrayOfRowArrays,
+        function (err, tsv) {
+          var utf16bytes = _stringToUtf16ByteArray(tsv)
+          var utf16bytesArray = new Uint8Array(utf16bytes.length)
+          utf16bytesArray.set(utf16bytes, 0)
+          var blob = new Blob([utf16bytesArray], {
+            type: 'text/tsv;charset=UTF-16LE;'
+          })
+          FileSaver.saveAs(blob, 'exportedTable' + namePrefix + '.tsv')
+        }
+      )
+    }
+
+    var data = []
+    var headings = []
+    var prefix = ''
+
+    if (options.columns === GridExportOptions.ALL) {
+      const allColumns = this.getAllColumns()
+      allColumns.forEach(function (column) {
+        if (column.exportable) {
+          headings.push(column.name)
+        }
+      })
+      prefix += 'AllColumns'
+    } else if (options.columns === GridExportOptions.VISIBLE) {
+      const visibleColumns = this.getVisibleColumns()
+      visibleColumns.forEach(function (column) {
+        if (column.exportable) {
+          headings.push(column.name)
+        }
+      })
+      prefix += 'VisibleColumns'
+    } else {
+      throw Error('Unsupported columns option: ' + options.columns)
+    }
+
+    const state = this.context.getState()
+    const props = this.context.getProps()
+
+    if (options.rows === GridExportOptions.ALL) {
+      if (props.rows) {
+        data = props.rows
+      } else if (props.loadRows) {
+        const loadedResult = await props.loadRows({
+          filters: state.filters,
+          page: 0,
+          pageSize: 1000000,
+          sort: state.sort,
+          sortDirection: state.sortDirection
+        })
+        if (_.isArray(loadedResult)) {
+          data = loadedResult
+        } else {
+          data = loadedResult.rows
+        }
+      }
+
+      prefix += 'AllRows'
+      _exportColumnsFromData(prefix, data, headings)
+    } else if (options.rows === GridExportOptions.VISIBLE) {
+      data = state.rows
+      prefix += 'VisibleRows'
+      _exportColumnsFromData(prefix, data, headings)
+    } else {
+      throw Error('Unsupported rows option: ' + options.columns)
+    }
+  }
+
   getAllColumns() {
     const { allColumns, columnsSorting } = this.context.getState()
 
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js b/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js
new file mode 100644
index 0000000000000000000000000000000000000000..09e5bea82fe4708e6282a98e41e19e901abaa5a3
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/common/grid/GridExportOptions.js
@@ -0,0 +1,11 @@
+const ALL = 'ALL'
+const VISIBLE = 'VISIBLE'
+const PLAIN_TEXT = 'PLAIN_TEXT'
+const RICH_TEXT = 'RICH_TEXT'
+
+export default {
+  ALL,
+  VISIBLE,
+  PLAIN_TEXT,
+  RICH_TEXT
+}
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx b/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx
index bf77b71d6cea5df921068e0488c8ce9962c008bc..af9f09aad6dabbaaa5be9379247122d8b2f21f54 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridExports.jsx
@@ -5,6 +5,7 @@ import Popover from '@material-ui/core/Popover'
 import SelectField from '@src/js/components/common/form/SelectField.jsx'
 import Button from '@src/js/components/common/form/Button.jsx'
 import Container from '@src/js/components/common/form/Container.jsx'
+import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js'
 import messages from '@src/js/common/messages.js'
 import logger from '@src/js/common/logger.js'
 
@@ -18,19 +19,24 @@ const styles = theme => ({
   }
 })
 
-const ALL = 'ALL'
-const VISIBLE = 'VISIBLE'
-const PLAIN_TEXT = 'PLAIN_TEXT'
-const RICH_TEXT = 'RICH_TEXT'
-
 class GridExports extends React.PureComponent {
   constructor(props) {
     super(props)
     this.state = {
-      el: null
+      el: null,
+      columns: {
+        value: GridExportOptions.VISIBLE
+      },
+      rows: {
+        value: GridExportOptions.VISIBLE
+      },
+      values: {
+        value: GridExportOptions.RICH_TEXT
+      }
     }
     this.handleOpen = this.handleOpen.bind(this)
     this.handleClose = this.handleClose.bind(this)
+    this.handleChange = this.handleChange.bind(this)
     this.handleExport = this.handleExport.bind(this)
   }
 
@@ -46,11 +52,24 @@ class GridExports extends React.PureComponent {
     })
   }
 
-  handleExport(action) {
+  handleChange(event) {
+    this.setState({
+      [event.target.name]: {
+        value: event.target.value
+      }
+    })
+  }
+
+  handleExport() {
     const { onExport } = this.props
     this.handleClose()
     if (onExport) {
-      onExport(action)
+      const { columns, rows, values } = this.state
+      onExport({
+        columns: columns.value,
+        rows: rows.value,
+        values: values.value
+      })
     }
   }
 
@@ -58,7 +77,7 @@ class GridExports extends React.PureComponent {
     logger.log(logger.DEBUG, 'GridExports.render')
 
     const { disabled, classes } = this.props
-    const { el } = this.state
+    const { el, columns, rows, values } = this.state
 
     return (
       <div className={classes.container}>
@@ -89,14 +108,14 @@ class GridExports extends React.PureComponent {
                 options={[
                   {
                     label: messages.get(messages.ALL),
-                    value: ALL
+                    value: GridExportOptions.ALL
                   },
                   {
                     label: messages.get(messages.VISIBLE),
-                    value: VISIBLE
+                    value: GridExportOptions.VISIBLE
                   }
                 ]}
-                value={VISIBLE}
+                value={columns.value}
                 variant='standard'
                 onChange={this.handleChange}
               />
@@ -108,14 +127,14 @@ class GridExports extends React.PureComponent {
                 options={[
                   {
                     label: messages.get(messages.ALL),
-                    value: ALL
+                    value: GridExportOptions.ALL
                   },
                   {
                     label: messages.get(messages.VISIBLE),
-                    value: VISIBLE
+                    value: GridExportOptions.VISIBLE
                   }
                 ]}
-                value={VISIBLE}
+                value={rows.value}
                 variant='standard'
                 onChange={this.handleChange}
               />
@@ -127,20 +146,24 @@ class GridExports extends React.PureComponent {
                 options={[
                   {
                     label: messages.get(messages.PLAIN_TEXT),
-                    value: PLAIN_TEXT
+                    value: GridExportOptions.PLAIN_TEXT
                   },
                   {
                     label: messages.get(messages.RICH_TEXT),
-                    value: RICH_TEXT
+                    value: GridExportOptions.RICH_TEXT
                   }
                 ]}
-                value={RICH_TEXT}
+                value={values.value}
                 variant='standard'
                 onChange={this.handleChange}
               />
             </div>
             <div className={classes.field}>
-              <Button label={messages.get(messages.EXPORT)} type='neutral' />
+              <Button
+                label={messages.get(messages.EXPORT)}
+                type='neutral'
+                onClick={this.handleExport}
+              />
             </div>
           </Container>
         </Popover>