diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js
--- a/lib/shared/dm-ops/dm-op-specs.js
+++ b/lib/shared/dm-ops/dm-op-specs.js
@@ -6,6 +6,7 @@
 import type { DMOperationSpec } from './dm-op-spec.js';
 import { joinThreadSpec } from './join-thread-spec.js';
 import { leaveThreadSpec } from './leave-thread-spec.js';
+import { removeMembersSpec } from './remove-members-spec.js';
 import { sendEditMessageSpec } from './send-edit-message-spec.js';
 import { sendReactionMessageSpec } from './send-reaction-message-spec.js';
 import { sendTextMessageSpec } from './send-text-message-spec.js';
@@ -22,4 +23,5 @@
   [dmOperationTypes.ADD_MEMBERS]: addMembersSpec,
   [dmOperationTypes.JOIN_THREAD]: joinThreadSpec,
   [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec,
+  [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec,
 });
diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/remove-members-spec.js
@@ -0,0 +1,90 @@
+// @flow
+
+import uuid from 'uuid';
+
+import type {
+  DMOperationSpec,
+  ProcessDMOperationUtilities,
+} from './dm-op-spec.js';
+import type { DMRemoveMembersOperation } from '../../types/dm-ops.js';
+import { messageTypes } from '../../types/message-types-enum.js';
+import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
+import { threadTypes } from '../../types/thread-types-enum.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type { ClientUpdateInfo } from '../../types/update-types.js';
+
+const removeMembersSpec: DMOperationSpec<DMRemoveMembersOperation> =
+  Object.freeze({
+    processDMOperation: async (
+      dmOperation: DMRemoveMembersOperation,
+      viewerID: string,
+      utilities: ProcessDMOperationUtilities,
+    ) => {
+      const { editorID, time, messageID, threadID, removedUserIDs } =
+        dmOperation;
+
+      const threadInfoOptional = utilities.threadInfos[threadID];
+      if (!threadInfoOptional || !threadInfoOptional.thick) {
+        // We can't perform this operation now. It should be queued for later.
+        return {
+          rawMessageInfos: [],
+          updateInfos: [],
+        };
+      }
+      const threadInfo: ThickRawThreadInfo = threadInfoOptional;
+
+      const removeMembersMessage = {
+        type: messageTypes.REMOVE_MEMBERS,
+        id: messageID,
+        threadID,
+        time,
+        creatorID: editorID,
+        removedUserIDs: [...removedUserIDs],
+      };
+
+      const removedUserIDsSet = new Set(removedUserIDs);
+      const viewerIsRemoved = removedUserIDsSet.has(viewerID);
+      const updateInfos: Array<ClientUpdateInfo> = [];
+      if (
+        viewerIsRemoved &&
+        (threadInfo.type !== threadTypes.THICK_SIDEBAR ||
+          (threadInfo.parentThreadID &&
+            !utilities.threadInfos[threadInfo.parentThreadID]))
+      ) {
+        updateInfos.push({
+          type: updateTypes.DELETE_THREAD,
+          id: uuid.v4(),
+          time,
+          threadID,
+        });
+      } else {
+        const updatedThreadInfo = {
+          ...threadInfo,
+          members: threadInfo.members.filter(
+            member => !removedUserIDsSet.has(member.id),
+          ),
+        };
+        updateInfos.push(
+          {
+            type: updateTypes.UPDATE_THREAD,
+            id: uuid.v4(),
+            time,
+            threadInfo: updatedThreadInfo,
+          },
+          {
+            type: updateTypes.UPDATE_THREAD_READ_STATUS,
+            id: uuid.v4(),
+            time,
+            threadID,
+            unread: true,
+          },
+        );
+      }
+      return {
+        rawMessageInfos: [removeMembersMessage],
+        updateInfos,
+      };
+    },
+  });
+
+export { removeMembersSpec };
diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js
--- a/lib/types/dm-ops.js
+++ b/lib/types/dm-ops.js
@@ -24,6 +24,7 @@
   ADD_MEMBERS: 'add_members',
   JOIN_THREAD: 'join_thread',
   LEAVE_THREAD: 'leave_thread',
+  REMOVE_MEMBERS: 'remove_members',
 });
 export type DMOperationType = $Values<typeof dmOperationTypes>;
 
@@ -223,6 +224,24 @@
     threadID: t.String,
   });
 
+export type DMRemoveMembersOperation = {
+  +type: 'remove_members',
+  +editorID: string,
+  +time: number,
+  +messageID: string,
+  +threadID: string,
+  +removedUserIDs: $ReadOnlyArray<string>,
+};
+export const dmRemoveMembersOperationValidator: TInterface<DMRemoveMembersOperation> =
+  tShape<DMRemoveMembersOperation>({
+    type: tString(dmOperationTypes.REMOVE_MEMBERS),
+    editorID: tUserID,
+    time: t.Number,
+    messageID: t.String,
+    threadID: t.String,
+    removedUserIDs: t.list(tUserID),
+  });
+
 export type DMOperation =
   | DMCreateThreadOperation
   | DMCreateSidebarOperation
@@ -231,7 +250,8 @@
   | DMSendEditMessageOperation
   | DMAddMembersOperation
   | DMJoinThreadOperation
-  | DMLeaveThreadOperation;
+  | DMLeaveThreadOperation
+  | DMRemoveMembersOperation;
 export const dmOperationValidator: TUnion<DMOperation> = t.union([
   dmCreateThreadOperationValidator,
   dmCreateSidebarOperationValidator,
@@ -241,6 +261,7 @@
   dmAddMembersOperationValidator,
   dmJoinThreadOperationValidator,
   dmLeaveThreadOperationValidator,
+  dmRemoveMembersOperationValidator,
 ]);
 
 export type DMOperationResult = {