diff --git a/openbis_ng_ui/src/js/common/consts/ids.js b/openbis_ng_ui/src/js/common/consts/ids.js
index d6148d853146c110543caa931a53bbb3fa9c0783..861192cce233b931e17fa3928068c6d966af2ffd 100644
--- a/openbis_ng_ui/src/js/common/consts/ids.js
+++ b/openbis_ng_ui/src/js/common/consts/ids.js
@@ -30,6 +30,7 @@ const PERSONAL_ACCESS_TOKEN_GRID_ID = 'personal_access_token_grid'
 // browsers
 const DATABASE_BROWSER_ID = 'database_browser'
 const TYPE_BROWSER_ID = 'type_browser'
+const USER_BROWSER_ID = 'user_browser'
 
 export default {
   // app
@@ -65,5 +66,6 @@ export default {
 
   // browsers
   DATABASE_BROWSER_ID,
-  TYPE_BROWSER_ID
+  TYPE_BROWSER_ID,
+  USER_BROWSER_ID
 }
diff --git a/openbis_ng_ui/src/js/components/users/Users.jsx b/openbis_ng_ui/src/js/components/users/Users.jsx
index f72928d31d5886ccb45c2f5821ff68227dabf53f..ff37c9faaea02f0e01c53274de1b241d5b0ec049 100644
--- a/openbis_ng_ui/src/js/components/users/Users.jsx
+++ b/openbis_ng_ui/src/js/components/users/Users.jsx
@@ -2,7 +2,7 @@ import React from 'react'
 import { withStyles } from '@material-ui/core/styles'
 import Content from '@src/js/components/common/content/Content.jsx'
 import ContentTab from '@src/js/components/common/content/ContentTab.jsx'
-import UserBrowser from '@src/js/components/users/browser/UserBrowser.jsx'
+import UserBrowser from '@src/js/components/users/browser2/UserBrowser.jsx'
 import UserSearch from '@src/js/components/users/search/UserSearch.jsx'
 import UserForm from '@src/js/components/users/form/user/UserForm.jsx'
 import UserGroupForm from '@src/js/components/users/form/usergroup/UserGroupForm.jsx'
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowser.jsx b/openbis_ng_ui/src/js/components/users/browser2/UserBrowser.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..697c64eea85af5269ce58344ea57b1c52bc30c82
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowser.jsx
@@ -0,0 +1,78 @@
+import _ from 'lodash'
+import React from 'react'
+import autoBind from 'auto-bind'
+import BrowserWithOpenbis from '@src/js/components/common/browser2/BrowserWithOpenbis.jsx'
+import BrowserButtonsAddRemove from '@src/js/components/common/browser2/BrowserButtonsAddRemove.jsx'
+import UserBrowserController from '@src/js/components/users/browser2/UserBrowserController.js'
+import AppController from '@src/js/components/AppController.js'
+import pages from '@src/js/common/consts/pages.js'
+import ids from '@src/js/common/consts/ids.js'
+import logger from '@src/js/common/logger.js'
+
+export class UserBrowser extends React.Component {
+  constructor(props) {
+    super(props)
+    autoBind(this)
+    this.controller = this.props.controller || new UserBrowserController()
+  }
+
+  componentDidMount() {
+    this.componentDidUpdate({})
+  }
+
+  componentDidUpdate(prevProps) {
+    if (!_.isEqual(this.props.selectedObject, prevProps.selectedObject)) {
+      this.controller.selectObject(this.props.selectedObject)
+    }
+
+    if (
+      !_.isEqual(
+        this.props.lastObjectModifications,
+        prevProps.lastObjectModifications
+      )
+    ) {
+      this.controller.reload(this.props.lastObjectModifications)
+    }
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'UserBrowser.render')
+
+    return (
+      <BrowserWithOpenbis
+        id={ids.USER_BROWSER_ID}
+        controller={this.controller}
+        renderFooter={this.renderFooter}
+        onSelectedChange={selectedObject => {
+          if (selectedObject) {
+            AppController.getInstance().objectOpen(
+              pages.USERS,
+              selectedObject.type,
+              selectedObject.id
+            )
+          }
+        }}
+      />
+    )
+  }
+
+  renderFooter() {
+    return (
+      <div>
+        <BrowserButtonsAddRemove
+          selectedObject={this.controller.getSelectedObject()}
+          addEnabled={this.controller.canAddNode()}
+          removeEnabled={this.controller.canRemoveNode()}
+          onAdd={this.controller.addNode}
+          onRemove={this.controller.removeNode}
+        />
+      </div>
+    )
+  }
+}
+
+export default AppController.getInstance().withState(() => ({
+  selectedObject: AppController.getInstance().getSelectedObject(pages.USERS),
+  lastObjectModifications:
+    AppController.getInstance().getLastObjectModifications()
+}))(UserBrowser)
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserConsts.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserConsts.js
new file mode 100644
index 0000000000000000000000000000000000000000..dff830968a40534605ce324ce061668237c1420e
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserConsts.js
@@ -0,0 +1,19 @@
+import messages from '@src/js/common/messages.js'
+
+const TYPE_ROOT = 'root'
+const TYPE_WARNING = 'warning'
+
+const TEXT_USERS = messages.get(messages.USERS)
+const TEXT_GROUPS = messages.get(messages.GROUPS)
+
+function nodeId(...parts) {
+  return parts.join('__')
+}
+
+export default {
+  nodeId,
+  TYPE_ROOT,
+  TYPE_WARNING,
+  TEXT_USERS,
+  TEXT_GROUPS
+}
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserController.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserController.js
new file mode 100644
index 0000000000000000000000000000000000000000..6e4d6ac55c0dcf583f8731218469936a4a4f9876
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserController.js
@@ -0,0 +1,42 @@
+import BrowserController from '@src/js/components/common/browser2/BrowserController.js'
+import UserBrowserControllerLoadNodePath from '@src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js'
+import UserBrowserControllerLoadNodes from '@src/js/components/users/browser2/UserBrowserControllerLoadNodes.js'
+import UserBrowserControllerAddNode from '@src/js/components/users/browser2/UserBrowserControllerAddNode.js'
+import UserBrowserControllerRemoveNode from '@src/js/components/users/browser2/UserBrowserControllerRemoveNode.js'
+import UserBrowserControllerReload from '@src/js/components/users/browser2/UserBrowserControllerReload.js'
+
+export default class UserBrowserController extends BrowserController {
+  async doLoadNodePath(params) {
+    return await new UserBrowserControllerLoadNodePath().doLoadNodePath(params)
+  }
+
+  async doLoadNodes(params) {
+    return await new UserBrowserControllerLoadNodes().doLoadNodes(params)
+  }
+
+  async reload(objectModifications) {
+    new UserBrowserControllerReload(this).reload(objectModifications)
+  }
+
+  canAddNode() {
+    return new UserBrowserControllerAddNode().canAddNode(
+      this.getSelectedObject()
+    )
+  }
+
+  async addNode() {
+    await new UserBrowserControllerAddNode().doAddNode(this.getSelectedObject())
+  }
+
+  canRemoveNode() {
+    return new UserBrowserControllerRemoveNode().canRemoveNode(
+      this.getSelectedObject()
+    )
+  }
+
+  async removeNode() {
+    await new UserBrowserControllerRemoveNode().doRemoveNode(
+      this.getSelectedObject()
+    )
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerAddNode.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerAddNode.js
new file mode 100644
index 0000000000000000000000000000000000000000..0719f60746f9f5e594dd48db9b68eb8145642a0f
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerAddNode.js
@@ -0,0 +1,28 @@
+import AppController from '@src/js/components/AppController.js'
+import objectType from '@src/js/common/consts/objectType.js'
+import pages from '@src/js/common/consts/pages.js'
+
+const NEW_OBJECT_TYPES = {
+  [objectType.USER]: objectType.NEW_USER,
+  [objectType.USER_GROUP]: objectType.NEW_USER_GROUP
+}
+
+export default class TypeBrowserControllerAddNode {
+  canAddNode(selectedObject) {
+    return (
+      selectedObject &&
+      selectedObject.type === objectType.OVERVIEW &&
+      NEW_OBJECT_TYPES[selectedObject.id]
+    )
+  }
+
+  async doAddNode(selectedObject) {
+    if (!this.canAddNode(selectedObject)) {
+      return
+    }
+    await AppController.getInstance().objectNew(
+      pages.USERS,
+      NEW_OBJECT_TYPES[selectedObject.id]
+    )
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd7e4f94bc524d2c752bbd5dfacbfca6e0a4d0c5
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodePath.js
@@ -0,0 +1,85 @@
+import UserBrowserConsts from '@src/js/components/users/browser2/UserBrowserConsts.js'
+import openbis from '@src/js/services/openbis.js'
+import objectType from '@src/js/common/consts/objectType.js'
+
+export default class TypeBrowserConstsLoadNodePath {
+  async doLoadNodePath(params) {
+    const { object } = params
+
+    if (object.type === objectType.OVERVIEW) {
+      if (object.id === objectType.USER) {
+        return this.createFolderPath(
+          objectType.USER,
+          UserBrowserConsts.TEXT_USERS
+        )
+      } else if (object.id === objectType.USER_GROUP) {
+        return this.createFolderPath(
+          objectType.USER_GROUP,
+          UserBrowserConsts.TEXT_GROUPS
+        )
+      }
+    } else if (object.type === objectType.USER) {
+      const id = new openbis.PersonPermId(object.id)
+      const fetchOptions = new openbis.PersonFetchOptions()
+
+      const persons = await openbis.getPersons([id], fetchOptions)
+      const person = persons[object.id]
+
+      return this.createNodePath(
+        object,
+        person,
+        objectType.USER,
+        UserBrowserConsts.TEXT_USERS
+      )
+    } else if (object.type === objectType.USER_GROUP) {
+      const id = new openbis.AuthorizationGroupPermId(object.id)
+      const fetchOptions = new openbis.AuthorizationGroupFetchOptions()
+
+      const groups = await openbis.getAuthorizationGroups([id], fetchOptions)
+      const group = groups[object.id]
+
+      return this.createNodePath(
+        object,
+        group,
+        objectType.USER_GROUP,
+        UserBrowserConsts.TEXT_GROUPS
+      )
+    } else {
+      return null
+    }
+  }
+
+  createFolderPath(folderObjectType, folderText) {
+    return [
+      {
+        id: UserBrowserConsts.nodeId(
+          UserBrowserConsts.TYPE_ROOT,
+          folderObjectType
+        ),
+        object: { type: objectType.OVERVIEW, id: folderObjectType },
+        text: folderText
+      }
+    ]
+  }
+
+  createNodePath(object, loadedObject, folderObjectType, folderText) {
+    if (loadedObject) {
+      const folderPath = this.createFolderPath(folderObjectType, folderText)
+      return [
+        ...folderPath,
+        {
+          id: UserBrowserConsts.nodeId(
+            UserBrowserConsts.TYPE_ROOT,
+            folderObjectType,
+            folderObjectType,
+            object.id
+          ),
+          object,
+          text: object.id
+        }
+      ]
+    } else {
+      return null
+    }
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodes.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodes.js
new file mode 100644
index 0000000000000000000000000000000000000000..f20488a2878bf612cfe4c3680ec8c183ef0871be
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerLoadNodes.js
@@ -0,0 +1,206 @@
+import _ from 'lodash'
+import UserBrowserConsts from '@src/js/components/users/browser2/UserBrowserConsts.js'
+import openbis from '@src/js/services/openbis.js'
+import objectType from '@src/js/common/consts/objectType.js'
+import messages from '@src/js/common/messages.js'
+import compare from '@src/js/common/compare.js'
+
+const LOAD_LIMIT = 100
+const TOTAL_LOAD_LIMIT = 500
+
+export default class UserBrowserConstsLoadNodesUnfiltered {
+  async doLoadNodes(params) {
+    const { node } = params
+
+    if (node.internalRoot) {
+      return {
+        nodes: [
+          {
+            id: UserBrowserConsts.TYPE_ROOT,
+            object: {
+              type: UserBrowserConsts.TYPE_ROOT
+            },
+            canHaveChildren: true
+          }
+        ]
+      }
+    } else if (node.object.type === UserBrowserConsts.TYPE_ROOT) {
+      const [users, groups] = await Promise.all([
+        this.searchUsers(params),
+        this.searchGroups(params)
+      ])
+
+      if (params.filter) {
+        const totalCount = users.totalCount + groups.totalCount
+
+        if (totalCount > TOTAL_LOAD_LIMIT) {
+          return this.tooManyResultsFound(node)
+        }
+      }
+
+      let nodes = [
+        this.createUsersNode(node, users),
+        this.createGroupsNode(node, groups)
+      ]
+
+      if (params.filter) {
+        nodes = nodes.filter(node => !_.isEmpty(node.children))
+      }
+
+      return {
+        nodes: nodes
+      }
+    } else if (node.object.type === objectType.OVERVIEW) {
+      let types = null
+
+      if (node.object.id === objectType.USER) {
+        types = await this.searchUsers(params)
+      } else if (node.object.id === objectType.USER_GROUP) {
+        types = await this.searchGroups(params)
+      }
+
+      if (types) {
+        return this.createNodes(node, types, node.object.id)
+      } else {
+        return {
+          nodes: []
+        }
+      }
+    } else {
+      return null
+    }
+  }
+
+  tooManyResultsFound(node) {
+    return {
+      nodes: [
+        {
+          id: UserBrowserConsts.nodeId(node.id, UserBrowserConsts.TYPE_WARNING),
+          message: {
+            type: 'warning',
+            text: messages.get(messages.TOO_MANY_FILTERED_RESULTS_FOUND)
+          },
+          selectable: false
+        }
+      ]
+    }
+  }
+
+  async searchUsers(params) {
+    const { filter, offset } = params
+
+    const criteria = new openbis.PersonSearchCriteria()
+    if (filter) {
+      criteria.withUserId().thatContains(filter)
+    }
+    const fetchOptions = new openbis.PersonFetchOptions()
+
+    const result = await openbis.searchPersons(criteria, fetchOptions)
+
+    return {
+      objects: result.getObjects().map(o => ({
+        id: o.getUserId(),
+        text: o.getUserId()
+      })),
+      totalCount: result.getTotalCount(),
+      filter,
+      offset
+    }
+  }
+
+  async searchGroups(params) {
+    const { filter, offset } = params
+
+    const criteria = new openbis.AuthorizationGroupSearchCriteria()
+    if (filter) {
+      criteria.withCode().thatContains(filter)
+    }
+    const fetchOptions = new openbis.AuthorizationGroupFetchOptions()
+
+    const result = await openbis.searchAuthorizationGroups(
+      criteria,
+      fetchOptions
+    )
+
+    return {
+      objects: result.getObjects().map(o => ({
+        id: o.getCode(),
+        text: o.getCode()
+      })),
+      totalCount: result.getTotalCount(),
+      filter,
+      offset
+    }
+  }
+
+  createUsersNode(parent, result) {
+    return this.createFolderNode(
+      parent,
+      result,
+      objectType.USER,
+      UserBrowserConsts.TEXT_USERS
+    )
+  }
+
+  createGroupsNode(parent, result) {
+    return this.createFolderNode(
+      parent,
+      result,
+      objectType.USER_GROUP,
+      UserBrowserConsts.TEXT_GROUPS
+    )
+  }
+
+  createFolderNode(parent, result, folderObjectType, folderText) {
+    const folderNode = {
+      id: UserBrowserConsts.nodeId(parent.id, folderObjectType),
+      text: folderText,
+      object: {
+        type: objectType.OVERVIEW,
+        id: folderObjectType
+      },
+      canHaveChildren: !!result,
+      selectable: true,
+      expanded: result && result.filter
+    }
+
+    if (result) {
+      folderNode.children = this.createNodes(
+        folderNode,
+        result,
+        folderObjectType
+      )
+    }
+
+    return folderNode
+  }
+
+  createNodes(parent, result, objectType) {
+    let objects = result.objects
+    objects.sort((o1, o2) => compare(o1.text, o2.text))
+    objects = objects.slice(result.offset, result.offset + LOAD_LIMIT)
+
+    let nodes = objects.map(object => ({
+      id: UserBrowserConsts.nodeId(parent.id, objectType, object.id),
+      text: object.text,
+      object: {
+        type: objectType,
+        id: object.id
+      }
+    }))
+
+    if (_.isEmpty(nodes)) {
+      return null
+    } else {
+      return {
+        nodes: nodes,
+        loadMore: {
+          offset: result.offset + nodes.length,
+          loadedCount: result.offset + nodes.length,
+          totalCount: result.totalCount,
+          append: true
+        }
+      }
+    }
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerReload.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerReload.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5e9af8443c519384fbca93ffabf36a50290fa1e
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerReload.js
@@ -0,0 +1,16 @@
+import BrowserControllerReload from '@src/js/components/common/browser2/BrowserControllerReload.js'
+import objectType from '@src/js/common/consts/objectType.js'
+import objectOperation from '@src/js/common/consts/objectOperation.js'
+
+export default class TypeBrowserControllerReload extends BrowserControllerReload {
+  constructor(controller) {
+    super(controller)
+  }
+
+  doGetObservedModifications() {
+    return {
+      [objectType.USER]: [objectOperation.CREATE, objectOperation.DELETE],
+      [objectType.USER_GROUP]: [objectOperation.CREATE, objectOperation.DELETE]
+    }
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerRemoveNode.js b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerRemoveNode.js
new file mode 100644
index 0000000000000000000000000000000000000000..4788233e1313f83b1644cf0370d5e59556e8b745
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/users/browser2/UserBrowserControllerRemoveNode.js
@@ -0,0 +1,81 @@
+import AppController from '@src/js/components/AppController.js'
+import objectType from '@src/js/common/consts/objectType.js'
+import openbis from '@src/js/services/openbis.js'
+import pages from '@src/js/common/consts/pages.js'
+import messages from '@src/js/common/messages.js'
+
+const REMOVABLE_OBJECT_TYPES = [objectType.USER, objectType.USER_GROUP]
+
+export default class TypeBrowserControllerRemoveNode {
+  canRemoveNode(selectedObject) {
+    return (
+      selectedObject && REMOVABLE_OBJECT_TYPES.includes(selectedObject.type)
+    )
+  }
+
+  async doRemoveNode(selectedObject) {
+    if (!this.canRemoveNode(selectedObject)) {
+      return
+    }
+
+    const { type, id } = selectedObject
+    const reason = 'deleted via ng_ui'
+
+    return this._prepareRemoveOperations(type, id, reason)
+      .then(operations => {
+        const options = new openbis.SynchronousOperationExecutionOptions()
+        options.setExecuteInOrder(true)
+        return openbis.executeOperations(operations, options)
+      })
+      .then(() => {
+        AppController.getInstance().objectDelete(pages.USERS, type, id)
+      })
+      .catch(error => {
+        if (
+          error &&
+          error.message &&
+          error.message.startsWith('Could not commit Hibernate transaction')
+        ) {
+          AppController.getInstance().errorChange(
+            messages.get(
+              messages.USERS_WHO_REGISTERED_SOME_DATA_CANNOT_BE_REMOVED
+            )
+          )
+        } else {
+          AppController.getInstance().errorChange(error)
+        }
+      })
+  }
+
+  _prepareRemoveOperations(type, id, reason) {
+    if (type === objectType.USER_GROUP) {
+      return this._prepareRemoveUserGroupOperations(id, reason)
+    } else if (type === objectType.USER) {
+      return this._prepareRemoveUserOperations(id, reason)
+    } else {
+      throw new Error('Unsupported type: ' + type)
+    }
+  }
+
+  _prepareRemoveUserGroupOperations(id, reason) {
+    const options = new openbis.AuthorizationGroupDeletionOptions()
+    options.setReason(reason)
+    return Promise.resolve([
+      new openbis.DeleteAuthorizationGroupsOperation(
+        [new openbis.AuthorizationGroupPermId(id)],
+        options
+      )
+    ])
+  }
+
+  _prepareRemoveUserOperations(id, reason) {
+    const options = new openbis.PersonDeletionOptions()
+    options.setReason(reason)
+    return Promise.resolve([
+      new openbis.DeletePersonsOperation(
+        [new openbis.PersonPermId(id)],
+        options
+      )
+    ])
+  }
+}
diff --git a/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js b/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js
index 6244d123d647ab363bf565764563ff93c264ced5..c69b316f85956a61336b49b9fd365653e1a604e0 100644
--- a/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js
+++ b/openbis_ng_ui/srcTest/js/components/users/wrapper/UsersWrapper.js
@@ -1,5 +1,5 @@
 import Content from '@src/js/components/common/content/Content.jsx'
-import UserBrowser from '@src/js/components/users/browser/UserBrowser.jsx'
+import { UserBrowser } from '@src/js/components/users/browser2/UserBrowser.jsx'
 import BaseWrapper from '@srcTest/js/components/common/wrapper/BaseWrapper.js'
 import BrowserWrapper from '@srcTest/js/components/common/browser/wrapper/BrowserWrapper.js'
 import ContentWrapper from '@srcTest/js/components/common/content/wrapper/ContentWrapper.js'