diff --git a/packages/react-interactions/events/src/dom/Tap.js b/packages/react-interactions/events/src/dom/Tap.js
index db6df9a70c93..0e175e5db957 100644
--- a/packages/react-interactions/events/src/dom/Tap.js
+++ b/packages/react-interactions/events/src/dom/Tap.js
@@ -344,7 +344,7 @@ function isActivePointer(
       const touch = getTouchById(nativeEvent, activePointerId);
       return touch != null;
     } else {
-      // accept all events that don't have ids
+      // accept all events that don't have pointer ids
       return true;
     }
   }
@@ -496,26 +496,29 @@ const responderImpl = {
       case 'pointerdown':
       case 'mousedown':
       case 'touchstart': {
-        if (hasPointerEvents) {
-          const pointerId = nativeEvent.pointerId;
-          state.activePointerId = pointerId;
-          // Make mouse and touch pointers consistent.
-          // Flow bug: https://github.com/facebook/flow/issues/8055
-          // $FlowExpectedError
-          eventTarget.releasePointerCapture(pointerId);
-        } else {
-          if (eventType === 'touchstart') {
-            const targetTouches = nativeEvent.targetTouches;
-            if (targetTouches.length > 0) {
-              state.activePointerId = targetTouches[0].identifier;
-            }
-          }
-          if (eventType === 'mousedown' && state.ignoreEmulatedEvents) {
-            return;
-          }
+        if (eventType === 'mousedown' && state.ignoreEmulatedEvents) {
+          return;
         }
 
         if (!state.isActive) {
+          if (hasPointerEvents) {
+            const pointerId = nativeEvent.pointerId;
+            state.activePointerId = pointerId;
+            // Make mouse and touch pointers consistent.
+            // Flow bug: https://github.com/facebook/flow/issues/8055
+            // $FlowExpectedError
+            eventTarget.releasePointerCapture(pointerId);
+          } else {
+            if (eventType === 'touchstart') {
+              const targetTouches = nativeEvent.targetTouches;
+              if (targetTouches.length === 1) {
+                state.activePointerId = targetTouches[0].identifier;
+              } else {
+                return;
+              }
+            }
+          }
+
           const activate = shouldActivate(event);
           const activateAuxiliary = isAuxiliary(nativeEvent.buttons, event);
 
@@ -547,6 +550,13 @@ const responderImpl = {
             state.initialPosition.y = gestureState.y;
             dispatchStart(context, props, state);
           }
+        } else if (
+          !isActivePointer(event, state) ||
+          (eventType === 'touchstart' && nativeEvent.targetTouches.length > 1)
+        ) {
+          // Cancel the gesture if a second pointer becomes active on the target.
+          state.isActive = false;
+          dispatchCancel(context, props, state);
         }
         break;
       }
diff --git a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
index 3cfa5e55b073..9c7d82891594 100644
--- a/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js
@@ -212,7 +212,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
       const buttons = buttonsType.auxiliary;
       const target = createEventTarget(ref.current);
       target.pointerdown({buttons, pointerType});
-      target.pointerup({pointerType});
+      target.pointerup({buttons, pointerType});
       expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
     });
 
@@ -221,7 +221,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
       const buttons = buttonsType.primary;
       const target = createEventTarget(ref.current);
       target.pointerdown({buttons, pointerType});
-      target.pointerup({metaKey: true, pointerType});
+      target.pointerup({buttons, metaKey: true, pointerType});
       expect(onAuxiliaryTap).toHaveBeenCalledTimes(1);
     });
   });
@@ -284,7 +284,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
       );
     });
 
-    test('second pointer down', () => {
+    test('second pointer on target', () => {
       const pointerType = 'touch';
       const target = createEventTarget(ref.current);
       const buttons = buttonsType.primary;
@@ -294,10 +294,7 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
         target.pointerdown({buttons, pointerId: 2, pointerType});
       } else {
         // TouchEvents
-        target.pointerdown([
-          {pointerId: 1, pointerType},
-          {pointerId: 2, pointerType},
-        ]);
+        target.pointerdown([{pointerId: 1}, {pointerId: 2}]);
       }
       expect(onTapStart).toHaveBeenCalledTimes(1);
     });
@@ -349,8 +346,10 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
 
     testWithPointerType('pointer up', pointerType => {
       const target = createEventTarget(ref.current);
-      target.pointerdown({buttons: buttonsType.primary, pointerType});
+      const buttons = buttonsType.primary;
+      target.pointerdown({buttons, pointerType});
       target.pointerup({
+        buttons,
         pageX: 10,
         pageY: 10,
         pointerType,
@@ -420,18 +419,40 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
       expect(onTapEnd).not.toBeCalled();
     });
 
+    if (hasPointerEvents) {
+      test('second pointer up off target', () => {
+        const pointerType = 'touch';
+        const target = createEventTarget(ref.current);
+        const offTarget = createEventTarget(container);
+        const buttons = buttonsType.primary;
+
+        target.pointerdown({buttons, pointerId: 1, pointerType});
+        offTarget.pointerdown({buttons, pointerId: 2, pointerType});
+        offTarget.pointerup({
+          buttons,
+          pageX: 10,
+          pageY: 10,
+          pointerId: 2,
+          pointerType,
+          x: 10,
+          y: 10,
+        });
+        expect(onTapEnd).toHaveBeenCalledTimes(0);
+      });
+    }
+
     test('ignored buttons and modifiers', () => {
       const target = createEventTarget(ref.current);
       const primary = buttonsType.primary;
       // right-click
       target.pointerdown({buttons: buttonsType.secondary});
-      target.pointerup();
+      target.pointerup({buttons: buttonsType.secondary});
       // middle-click
       target.pointerdown({buttons: buttonsType.auxiliary});
-      target.pointerup();
+      target.pointerup({buttons: buttonsType.auxiliary});
       // pen eraser
       target.pointerdown({buttons: buttonsType.eraser});
-      target.pointerup();
+      target.pointerup({buttons: buttonsType.eraser});
       // alt-click
       target.pointerdown({buttons: primary});
       target.pointerup({altKey: true});
@@ -533,6 +554,21 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
       // No extra 'onTapUpdate' calls when the pointer is outside the target
       expect(onTapUpdate).toHaveBeenCalledTimes(1);
     });
+
+    if (hasPointerEvents) {
+      test('second pointer off target', () => {
+        const pointerType = 'touch';
+        const target = createEventTarget(ref.current);
+        const offTarget = createEventTarget(container);
+        const buttons = buttonsType.primary;
+        target.pointerdown({buttons, pointerId: 1, pointerType});
+        offTarget.pointerdown({buttons, pointerId: 2, pointerType});
+        target.pointermove({pointerId: 1, pointerType, x: 10, y: 10});
+        expect(onTapUpdate).toHaveBeenCalledTimes(1);
+        offTarget.pointermove({pointerId: 2, pointerType, x: 10, y: 10});
+        expect(onTapUpdate).toHaveBeenCalledTimes(1);
+      });
+    }
   });
 
   describe('onTapChange', () => {
@@ -652,6 +688,32 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
       expect(onTapUpdate).not.toBeCalled();
     });
 
+    test('second pointer on target', () => {
+      const pointerType = 'touch';
+      const target = createEventTarget(ref.current);
+      const buttons = buttonsType.primary;
+      target.pointerdown({buttons, pointerId: 1, pointerType});
+      if (hasPointerEvents) {
+        target.pointerdown({buttons, pointerId: 2, pointerType});
+      } else {
+        // TouchEvents
+        target.pointerdown([{pointerId: 1}, {pointerId: 2}]);
+      }
+      expect(onTapCancel).toHaveBeenCalledTimes(1);
+    });
+
+    if (hasPointerEvents) {
+      test('second pointer off target', () => {
+        const pointerType = 'touch';
+        const target = createEventTarget(ref.current);
+        const offTarget = createEventTarget(container);
+        const buttons = buttonsType.primary;
+        target.pointerdown({buttons, pointerId: 1, pointerType});
+        offTarget.pointerdown({buttons, pointerId: 2, pointerType});
+        expect(onTapCancel).toHaveBeenCalledTimes(0);
+      });
+    }
+
     testWithPointerType('pointer move outside target', pointerType => {
       const downTarget = createEventTarget(ref.current);
       const upTarget = createEventTarget(container);
diff --git a/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js b/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js
index 344d9700181c..9041a2d2cdc7 100644
--- a/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js
+++ b/packages/react-interactions/events/src/dom/testing-library/domEventSequences.js
@@ -18,6 +18,8 @@ function getPointerType(payload) {
   let pointerType = 'mouse';
   if (payload != null && payload.pointerType != null) {
     pointerType = payload.pointerType;
+  } else if (Array.isArray(payload)) {
+    pointerType = 'touch';
   }
   return pointerType;
 }
@@ -77,31 +79,36 @@ export function pointercancel(target, payload) {
 export function pointerdown(target, defaultPayload) {
   const dispatch = arg => target.dispatchEvent(arg);
   const pointerType = getPointerType(defaultPayload);
-  const payload = {buttons: buttonsType.primary, ...defaultPayload};
 
-  if (pointerType === 'mouse') {
-    if (hasPointerEvent()) {
-      dispatch(domEvents.pointerover(payload));
-      dispatch(domEvents.pointerenter(payload));
-    }
-    dispatch(domEvents.mouseover(payload));
-    dispatch(domEvents.mouseenter(payload));
-    if (hasPointerEvent()) {
-      dispatch(domEvents.pointerdown(payload));
-    }
-    dispatch(domEvents.mousedown(payload));
-    if (document.activeElement !== target) {
-      dispatch(domEvents.focus());
-    }
+  if (Array.isArray(defaultPayload)) {
+    // Arrays are for multi-touch only
+    dispatch(domEvents.touchstart(defaultPayload));
   } else {
-    if (hasPointerEvent()) {
-      dispatch(domEvents.pointerover(payload));
-      dispatch(domEvents.pointerenter(payload));
-      dispatch(domEvents.pointerdown(payload));
-    }
-    dispatch(domEvents.touchstart(payload));
-    if (hasPointerEvent()) {
-      dispatch(domEvents.gotpointercapture(payload));
+    const payload = {buttons: buttonsType.primary, ...defaultPayload};
+    if (pointerType === 'mouse') {
+      if (hasPointerEvent()) {
+        dispatch(domEvents.pointerover(payload));
+        dispatch(domEvents.pointerenter(payload));
+      }
+      dispatch(domEvents.mouseover(payload));
+      dispatch(domEvents.mouseenter(payload));
+      if (hasPointerEvent()) {
+        dispatch(domEvents.pointerdown(payload));
+      }
+      dispatch(domEvents.mousedown(payload));
+      if (document.activeElement !== target) {
+        dispatch(domEvents.focus());
+      }
+    } else {
+      if (hasPointerEvent()) {
+        dispatch(domEvents.pointerover(payload));
+        dispatch(domEvents.pointerenter(payload));
+        dispatch(domEvents.pointerdown(payload));
+      }
+      dispatch(domEvents.touchstart(payload));
+      if (hasPointerEvent()) {
+        dispatch(domEvents.gotpointercapture(payload));
+      }
     }
   }
 }
@@ -153,13 +160,14 @@ export function pointermove(target, payload) {
   }
 }
 
-export function pointerup(target, defaultPayload = {}) {
+export function pointerup(target, payload) {
   const dispatch = arg => target.dispatchEvent(arg);
-  const pointerType = getPointerType(defaultPayload);
-  // eslint-disable-next-line no-unused-vars
-  const {buttons, ...payload} = defaultPayload;
+  const pointerType = getPointerType(payload);
 
-  if (pointerType === 'mouse') {
+  if (Array.isArray(payload)) {
+    // Arrays are for multi-touch only
+    dispatch(domEvents.touchend(payload));
+  } else if (pointerType === 'mouse') {
     if (hasPointerEvent()) {
       dispatch(domEvents.pointerup(payload));
     }
@@ -175,7 +183,6 @@ export function pointerup(target, defaultPayload = {}) {
     dispatch(domEvents.touchend(payload));
     dispatch(domEvents.mouseover(payload));
     dispatch(domEvents.mousemove(payload));
-    // NOTE: the value of 'buttons' for 'mousedown' must not be 0
     dispatch(domEvents.mousedown(payload));
     if (document.activeElement !== target) {
       dispatch(domEvents.focus());
diff --git a/packages/react-interactions/events/src/dom/testing-library/domEvents.js b/packages/react-interactions/events/src/dom/testing-library/domEvents.js
index 85bc9600e37e..92391b270ddc 100644
--- a/packages/react-interactions/events/src/dom/testing-library/domEvents.js
+++ b/packages/react-interactions/events/src/dom/testing-library/domEvents.js
@@ -422,9 +422,14 @@ export function pointerup(payload) {
  */
 
 export function mousedown(payload) {
+  // The value of 'buttons' for 'mousedown' must not be 0
+  const buttons =
+    payload == null || payload.buttons === 0
+      ? buttonsType.primary
+      : payload.buttons;
   return createMouseEvent('mousedown', {
-    buttons: buttonsType.primary,
     ...payload,
+    buttons,
   });
 }
 
